<?php

namespace Xin\Menu;

use Xin\Capsule\WithConfig;
use Xin\Menu\Contracts\Generator;
use Xin\Menu\Contracts\Menu as MenuContract;
use Xin\Menu\Contracts\Repository;
use Xin\Support\Arr;
use Xin\Support\Path;

class Menu implements MenuContract
{
	use WithConfig;

	/**
	 * @var Repository
	 */
	protected $repository;

	/**
	 * @var Generator
	 */
	protected $generator;

	/**
	 * @var $config
	 */
	protected $config;

	/**
	 * @var array
	 */
	protected $data = [];

	/**
	 * @var bool
	 */
	protected $isLoaded = false;

	/**
	 * @param array $config
	 * @param Repository|null $repository
	 * @param Generator|null $generator
	 */
	public function __construct(array $config, Repository $repository = null, Generator $generator = null)
	{
		$this->config = $config;
		$this->repository = $repository;
		$this->generator = $generator;

		$this->load();
	}

	/**
	 * @return string
	 */
	protected function getBasePath()
	{
		return $this->getConfig('base_path');
	}

	/**
	 * @return string
	 */
	protected function getTargetPath()
	{
		return $this->getConfig('target_path');
	}

	/**
	 * 初始化加载文件
	 * @return void
	 */
	protected function load()
	{
		if ($this->isLoaded) {
			return;
		}

		$this->isLoaded = true;

		$basePath = $this->getBasePath();
		$targetPath = $this->getTargetPath();

		$append = [];
		if (!empty($targetPath) && file_exists($targetPath)) {
			$data = require_once $targetPath;
			if (is_bool($data)) {
				$data = [];
			}

			$this->data = $data;
		} elseif (!empty($basePath) && file_exists($basePath)) {
			$data = require_once $basePath;
			if (is_bool($data)) {
				$data = [];
			}

			$append['system'] = 1;
			$this->appends($data, null, $append);
		}
	}

	/**
	 * 获取菜单数据
	 * @param callable|null $filter
	 * @return array|mixed
	 */
	public function get(callable $filter = null)
	{
		if ($filter) {
			return Util::eachTreeFilter($this->data, $filter);
		}

		return $this->data;
	}

	/**
	 * 获取菜单数据
	 * @param string $app
	 * @return array
	 */
	public function getByApp(string $app)
	{
		return $this->get(function ($item) use ($app) {
			if (isset($item['app'])) {
				return $item['app'] == $app;
			}

			return false;
		});
	}

	/**
	 * 返回菜单列表数据
	 * @param callable|null $filter
	 * @return array
	 */
	public function toList(callable $filter = null)
	{
		$menus = $this->get($filter);

		$result = [];
		Util::eachTree($menus, function ($item) use (&$result) {
			unset($item['child']);
			$result[] = $item;
		});

		return $result;
	}

	/**
	 * 把一个文件添加到菜单数据中
	 * @param string $file
	 * @param string|null $app
	 * @param array $append
	 * @return void
	 */
	public function appendFile(string $file, string $app = null, array $append = [])
	{
		$suffix = Path::suffix($file);
		if ($suffix === 'php') {
			$menus = require_once $file;
		} elseif ($suffix === 'json') {
			$menus = json_decode(file_get_contents($file), true);
		} else {
			throw new \InvalidArgumentException("不支持的文件：" . $file);
		}

		$this->appends($menus, $app, $append);
	}

	/**
	 * 追加一组菜单数据
	 * @param array $menus
	 * @param string|null $app
	 * @param array $append
	 * @return void
	 */
	public function appends(array $menus, string $app = null, array $append = [])
	{
		foreach ($menus as $menu) {
			$this->append($menu, $app, $append);
		}

		$this->sort($this->data);
	}

	/**
	 * 插入一个菜单
	 *
	 * @param array $menu
	 * @param string $app
	 * @param array $append
	 */
	public function append(array $menu, string $app = null, array $append = [])
	{
		$app = empty($app) ? '' : $app;

		$menu = array_merge($this->buildDefaultMenuOptions($menu), $menu, $append);
		$menu['app'] = $app;

		if (isset($menu['child'])) {
			Util::eachTree($menu['child'], function (&$item) use ($app, $append) {
				$item = array_merge($this->buildDefaultMenuOptions($item), $item, $append);
				$item['app'] = $app;
			});
			$this->sort($menu['child']);
		}

		if (isset($menu['parent'])) {
			foreach ($this->data as &$item) {
				if ($item['url'] == $menu['parent']) {
					if (!isset($item['child'])) {
						$item['child'] = [];
					}

					$item['child'][] = $menu;
					break;
				}
			}
			unset($item);
		} else {
			$this->data[] = $menu;
		}
	}

	/**
	 * 编译一个默认的菜单配置参数
	 * @param array $menu
	 * @return array
	 */
	protected function buildDefaultMenuOptions(array $menu)
	{
		$router = str_replace(".", "/", $menu['url']);
		return [
			'show'       => false,
			'link'       => $menu['link'] ?? (isset($menu['child']) ? 0 : 1) ?? 1,
			'sort'       => 0,
			'icon'       => '',
			'only_admin' => false,
			'only_dev'   => false,
			'router'     => $router,
			'view'       => $router,
		];
	}

	/**
	 * 移除对应的菜单
	 */
	public function forget($condition)
	{
		if (is_numeric($condition)) {
			unset($this->data[$condition]);
		} elseif (is_callable($condition)) {
			Util::eachTreeFilter($this->data, $condition);
		} elseif (is_string($condition)) {
			Util::eachTreeFilter($this->data, function ($item) use ($condition) {
				return $condition != $item['url'];
			});
		} else {
			Util::eachTreeFilter($this->data, function ($item) use ($condition) {
				return !Arr::where($item, $condition);
				// return empty(array_diff_assoc($item, $condition));
			});
		}
	}

	/**
	 * 根据app名称进行移除菜单
	 * @param string $app
	 * @return void
	 */
	public function forgetByApp(string $app)
	{
		$this->forget(function ($item) use ($app) {
			if (isset($item['app'])) {
				return $item['app'] != $app;
			}
			return true;
		});
	}

	/**
	 * 写入数据
	 *
	 * @param string|null $targetPath
	 */
	public function export(string $targetPath = null, callable $filter = null)
	{
		$targetPath = $targetPath ?: $this->getTargetPath();
		if (empty($targetPath)) {
			throw new \RuntimeException("'target_path' not configure.");
		}

		$content = "<?php\nreturn " . var_export($this->data, true) . ";";
		file_put_contents($targetPath, $content);
	}

	/**
	 * 数组排序
	 *
	 * @param array $list
	 */
	protected function sort(array &$list)
	{
		usort($list, function ($it1, $it2) {
			$sort1 = $it1['sort'] ?? 0;
			$sort2 = $it2['sort'] ?? 0;

			return $sort1 == $sort2 ? 0 : ($sort1 > $sort2 ? 1 : -1);
		});
	}

	/**
	 * 清理菜单
	 * @param string|null $app
	 * @return void
	 */
	public function clear(string $app = null, bool $isPersistent = true)
	{
		if ($app) {
			$this->forgetByApp($app);
			$this->export();
		} else {
			$targetPath = $this->get('target_path');
			if (file_exists($targetPath)) {
				unlink($targetPath);
			}
		}

		if ($isPersistent) {
			if ($app) {
				$this->repository->deleteByApp($app);
			} else {
				$this->repository->deleteAll();
			}
		}
	}

	/**
	 * 获取仓库数据，并可以根据app进行筛选
	 * @param string|null $app
	 * @return array
	 */
	protected function getRepositoryItemsByApp(string $app = null)
	{
		if (empty($app)) {
			$this->repository->all();
		}

		return $this->repository->all([
			'app' => $app,
		]);
	}


	/**
	 * @inheritDoc
	 * @param array $menus
	 * @param string|null $app
	 * @param array $append
	 */
	public function puts(array $menus, string $app = null, array $append = [])
	{
		$databaseMenus = $this->repository->all();
		$databaseMenusUrlMaps = array_column($databaseMenus, 'id', 'url');
		$databaseMenusUrls = array_keys($databaseMenusUrlMaps);

		$this->appendToDatabase($menus, $databaseMenusUrls, $databaseMenusUrlMaps);
		$this->appends($menus);
	}

	/**
	 * 生成菜单
	 *
	 * @param string $rule
	 * @param array $options
	 * @return array
	 */
	public function generate(string $rule, array $options = [])
	{
		if (!$this->repository) {
			$menus = $this->get($options['filter'] ?? null);
		} else {
			$menus = $this->repository->all();
			$menus = Arr::tree($menus);
			if (isset($options['filter'])) {
				$menus = Util::eachTreeFilter($menus, $options['filter']);
			}
		}

		return $this->generator->generate($menus, $rule, $options);
	}

	/**
	 * 刷新菜单
	 *
	 * @param string|null $app
	 * @return bool
	 */
	public function refresh(string $app = null, bool $detaching = true)
	{
		if (!$this->repository) {
			$this->export();
			return true;
		} else {
			$this->export();
		}

		$databaseMenus = $this->getRepositoryItemsByApp($app);
		$databaseMenusUrlMaps = array_column($databaseMenus, 'id', 'url');
		$databaseMenusUrls = array_keys($databaseMenusUrlMaps);

		$sourceMenus = $app ? $this->getByApp($app) : $this->get();
		$sourceMenusUrls = [];
		Util::eachTree($sourceMenus, function ($item) use (&$sourceMenusUrls) {
			$sourceMenusUrls[] = $item['url'];
		});

		$detached = array_diff($databaseMenusUrls, $sourceMenusUrls);
		if ($detaching && count($detached) > 0) {
			$detachIds = array_values(array_map(function ($url) use ($databaseMenusUrlMaps) {
				return $databaseMenusUrlMaps[$url];
			}, $detached));
			$this->repository->delete($detachIds);
		}

		$this->appendToDatabase($sourceMenus, $databaseMenusUrls, $databaseMenusUrlMaps);

		return true;
	}

	/**
	 * 更新数据到数据库中
	 * @param array $menus
	 * @param array $databaseMenusUrls
	 * @param array $databaseMenusUrlMaps
	 * @return void
	 */
	protected function appendToDatabase(array $menus, array $databaseMenusUrls, array $databaseMenusUrlMaps)
	{
		Util::eachTree($menus, function ($attributes, $parent) use ($databaseMenusUrls, &$databaseMenusUrlMaps) {
			$key = $attributes['url'];

			$parentKey = $attributes['parent'] ?? ($parent ? $parent['url'] : '');
			unset($attributes['parent']);
			unset($attributes['child']);

			if (!in_array($key, $databaseMenusUrls)) {
				$attributes['pid'] = $databaseMenusUrlMaps[$parentKey] ?? 0;
				$menu = $this->repository->insert($attributes);
				$databaseMenusUrlMaps[$key] = $menu['id'];
			} elseif (count($attributes) > 0) {
				$id = $databaseMenusUrlMaps[$key];
				$this->repository->update($id, $attributes);
			}
		});
	}
}
